﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Migrator.Entities;
using Migrator.Util;
using Migrator.Packets;
using Migrator.Services.Registration.Packets;
using Migrator.Core;
using Migrator.Services.RouteOptimization.Packets;

namespace Migrator.Services.MobileAgents
{
    /// <summary>
    /// Provides the foreign agent capability
    /// </summary>
    public class ForeignAgent : IService
    {
        public ForeignAgent(Node client)
        {
            this.Client = client;
        }

        public void Update()
        {
            RejectExpiredPending();

            this.visitorsTable.RemoveExpired(Simulator.Instance.Clock);
        }

        public bool Filter(IPPacket packet)
        {
            RegistrationRequestPacket regPacket = packet as RegistrationRequestPacket;
            if (regPacket != null)
            {
                // Don't handle the request if I'm the home agent
                return this.Client.IPAddress != regPacket.HomeAgent;
            }

            if (packet is RegistrationReplyPacket)
                return true;

            if (packet is BindingUpdatePacket)
                return true;

            // Encapsulated packet
            if (packet.Payload is IPPacket)
                return true;

            return false;
        }

        public void Handle(IPPacket packet)
        {
            RegistrationRequestPacket regPacket = packet as RegistrationRequestPacket;
            if (regPacket != null)
                this.Handle(regPacket);

            RegistrationReplyPacket reply = packet as RegistrationReplyPacket;
            if (reply != null)
                this.Handle(reply);

            BindingUpdatePacket update = packet as BindingUpdatePacket;
            if (update != null)
                this.Handle(update);

            IPPacket inner = packet.Payload as IPPacket;
            if (inner != null)
            {
                // Deliver encapsulated packets to the mobile node
                if (this.visitorsTable.Contains(inner.DestinationIP))
                {
                    this.Client.SendLinkLayer(inner, this.visitorsTable[inner.DestinationIP].MACAddress);
                }
                else
                {
                    // Node has deregistered? Send a binding warning message if
                    // route optimization is enabled.
                    this.Client.Send(this.InitializeBindingWarningPacket(inner.DestinationIP, packet.SourceIP));
                }
            }
        }


        public IService Configure(params object[] configuration)
        {
            this.Enabled = true;
            return this;
        }

        public bool Enabled
        {
            get;
            set;
        }

        public Node Client
        {
            get;
            private set;
        }

        public ushort MaxRegistrationLifetime
        {
            get { return this.maxLifetime; }
            set { this.maxLifetime = value; }
        }

        private void Handle(RegistrationRequestPacket packet)
        {
            // Reply to send in case the request is denied
            RegistrationReplyPacket reply = new RegistrationReplyPacket(this.Client, packet.SourceIP, packet.SourcePort);
            reply.HomeAddress = packet.HomeAddress;
            reply.HomeAgent = packet.HomeAgent;
            reply.Lifetime = packet.Lifetime;

            // Requests with the 'D' bit set to 0, and specifying a care-of 
            // address not offered by the foreign agent, MUST be rejected 
            // with code 77 (invalid care-of address).
            if (packet.D == false && packet.CareOfAddress != this.Client.IPAddress)
            {
                reply.Code = 77;
            }
            else if (packet.Lifetime > this.maxLifetime)
            {
                // If the requested lifetime is too long...
                reply.Code = 69;
                reply.Lifetime = this.maxLifetime;
            }

            // If the request has been denied...
            if (reply.Code != 0)
            {
                // Send the error reply to the mobile node
                this.Client.SendLinkLayer(reply, packet.Source.MACAddress);
            }
            else
            {
                // add a new entry to the pending registrations table
                // Note that pending registrations expire after 7 steps
                VisitorsTable.VisitorsEntry entry = new VisitorsTable.VisitorsEntry(packet.HomeAddress, packet.HomeAgent, packet.Source.MACAddress, Simulator.Instance.Clock + 7, packet.Lifetime, packet.SourcePort);
                this.pendingRegistrations.Add(entry);

                // Relay the request to the home agent
                RegistrationRequestPacket toHomeAgent = new RegistrationRequestPacket(this.Client, packet.HomeAgent);
                toHomeAgent.B = packet.B;
                toHomeAgent.CareOfAddress = packet.CareOfAddress;
                toHomeAgent.D = packet.D;
                toHomeAgent.G = packet.G;
                toHomeAgent.HomeAddress = packet.HomeAddress;
                toHomeAgent.HomeAgent = packet.HomeAgent;
                toHomeAgent.Lifetime = packet.Lifetime;
                toHomeAgent.M = packet.M;
                toHomeAgent.S = packet.S;
                toHomeAgent.T = packet.T;

                this.Client.Send(toHomeAgent);
            }
        }

        private void Handle(RegistrationReplyPacket packet)
        {
            // If no such pending Request is found, silently discard
            if (!pendingRegistrations.Contains(packet.HomeAddress))
                return;

            var pending = pendingRegistrations[packet.HomeAddress];
            MACAddress nodeMACAddress = pending.MACAddress;

            // If request was denied by the home agent
            if (packet.Code >= 128)
            {
                pendingRegistrations.Remove(pending.HomeAddress);
            }
            else if (packet.Lifetime == 0)
            {
                // Deregister mobile node
                this.visitorsTable.Remove(packet.HomeAddress);
            }
            else
            {
                // Whoo-hoo, registration was accepted by the home agent.
                // Update the visitors list.
                VisitorsTable.VisitorsEntry entry = new VisitorsTable.VisitorsEntry(pending.HomeAddress, 
                                                                                    pending.HomeAgent, 
                                                                                    pending.MACAddress, 
                                                                                    Simulator.Instance.Clock + Math.Min(pending.RequestedLifetime, this.maxLifetime), 
                                                                                    packet.Lifetime,
                                                                                    pending.RequestSourcePort);
                this.visitorsTable.Add(entry);
                this.pendingRegistrations.Remove(pending.HomeAddress);

            }

            RegistrationReplyPacket reply = new RegistrationReplyPacket(this.Client, packet.HomeAddress, packet.DestinationPort);
            reply.Code = packet.Code;
            reply.HomeAddress = packet.HomeAddress;
            reply.HomeAgent = packet.HomeAgent;
            reply.Lifetime = Math.Min(pending.RequestedLifetime, this.maxLifetime);

            this.Client.SendLinkLayer(reply, nodeMACAddress);
        }

        private void Handle(BindingUpdatePacket packet)
        {
            // If I have some visitor with this care-of address,
            // remove it!
            if (this.visitorsTable.Contains(packet.HomeAddress))
            {
                this.visitorsTable.Remove(packet.HomeAddress);
            }
        }

        private IPPacket InitializeBindingWarningPacket(IPAddress homeAddress, IPAddress destinationIP)
        {
            BindingWarningPacket packet = new BindingWarningPacket(this.Client, destinationIP);
            packet.HomeAddress = homeAddress;

            return packet;
        }

        private void RejectExpiredPending()
        {
            var removed = this.pendingRegistrations.RemoveExpired(Simulator.Instance.Clock);
            foreach (var entry in removed)
            {
                // Reject the request
                RegistrationReplyPacket reply = new RegistrationReplyPacket(this.Client, entry.HomeAddress, entry.RequestSourcePort);
                reply.HomeAddress = entry.HomeAddress;
                reply.HomeAgent = entry.HomeAgent;
                reply.Lifetime = entry.RequestedLifetime;
                reply.Code = 78;

                this.Client.SendLinkLayer(reply, entry.MACAddress);
            }
        }

        public VisitorsTable VisitorsTable
        {
            get { return this.visitorsTable; }
        }

        /// <summary>
        /// Foreign-agent visitors table
        /// </summary>
        private VisitorsTable visitorsTable = new VisitorsTable();

        /// <summary>
        /// Pending registrations
        /// </summary>
        private VisitorsTable pendingRegistrations = new VisitorsTable();

        /// <summary>
        /// Maximum allowed lifetime
        /// </summary>
        private ushort maxLifetime = 150;
    }
}
